package rag

import (
	"context"
	"fmt"
	"log/slog"
	"os"
	"path/filepath"

	"github.com/docker/cagent/pkg/rag/database"
	"github.com/docker/cagent/pkg/rag/fusion"
	"github.com/docker/cagent/pkg/rag/strategy"
)

// Config represents RAG manager configuration in domain terms,
// independent of any particular config schema version.
type Config struct {
	Description     string
	Docs            []string
	Results         ResultsConfig
	FusionConfig    *FusionConfig
	StrategyConfigs []strategy.Config
}

// ResultsConfig captures result-postprocessing behavior for the manager.
type ResultsConfig struct {
	Limit             int  // Maximum number of results to return (top K)
	Deduplicate       bool // Remove duplicate entries based on final content
	IncludeScore      bool // Include relevance scores in results (if/when used)
	ReturnFullContent bool // Return full document content instead of just matched chunks
}

// Manager orchestrates RAG operations using pluggable strategies
// Supports both single-strategy and hybrid multi-strategy retrieval with fusion
type Manager struct {
	name            string
	config          Config
	strategies      map[string]strategy.Strategy // Map of strategy name -> strategy instance
	strategyConfigs map[string]strategy.Config   // Store configs for per-strategy operations
	fusion          fusion.Fusion                // Fusion strategy for combining multi-strategy results
	events          <-chan strategy.Event        // Shared event channel from strategies
}

// FusionConfig holds configuration for result fusion
type FusionConfig struct {
	Strategy string             // "rrf", "weighted", "max"
	K        int                // RRF parameter
	Weights  map[string]float64 // Strategy weights
}

// New creates a new RAG manager with one or more strategies.
// Pass multiple strategy configs to enable hybrid retrieval.
// The strategyEvents channel should be shared across all strategies for this manager.
func New(_ context.Context, name string, config Config, strategyEvents <-chan strategy.Event) (*Manager, error) {
	if len(config.StrategyConfigs) == 0 {
		return nil, fmt.Errorf("at least one strategy required")
	}

	// Build strategies map from configs
	strategyMap := make(map[string]strategy.Strategy)
	for _, strategyCfg := range config.StrategyConfigs {
		strategyMap[strategyCfg.Name] = strategyCfg.Strategy
	}

	// Create fusion strategy if multiple strategies
	var fusionStrategy fusion.Fusion
	if len(config.StrategyConfigs) > 1 {
		fusionConfig := config.FusionConfig
		if fusionConfig == nil {
			// Default to RRF
			fusionConfig = &FusionConfig{Strategy: "rrf"}
		}

		fusionCfg := fusion.Config{
			Strategy: fusionConfig.Strategy,
			K:        fusionConfig.K,
			Weights:  fusionConfig.Weights,
		}

		var err error
		fusionStrategy, err = fusion.New(fusionCfg)
		if err != nil {
			return nil, fmt.Errorf("failed to create fusion strategy: %w", err)
		}

		// Ensure fusion was actually created
		if fusionStrategy == nil {
			return nil, fmt.Errorf("fusion strategy is nil after creation (this is a bug)")
		}
	}

	// Store strategy configs for later use
	strategyConfigMap := make(map[string]strategy.Config)
	for _, sc := range config.StrategyConfigs {
		strategyConfigMap[sc.Name] = sc
	}

	m := &Manager{
		name:            name,
		config:          config,
		strategies:      strategyMap,
		strategyConfigs: strategyConfigMap,
		fusion:          fusionStrategy,
		events:          strategyEvents,
	}

	return m, nil
}

// Initialize indexes all documents using all configured strategies
// Each strategy indexes its own document set (shared + strategy-specific)
// Strategies are initialized in parallel for better performance
func (m *Manager) Initialize(ctx context.Context) error {
	slog.Debug("[RAG Manager] Starting initialization",
		"rag_name", m.name,
		"num_strategies", len(m.strategies))

	// Initialize strategies in parallel to avoid blocking
	type result struct {
		strategyName string
		err          error
	}

	resultsChan := make(chan result, len(m.strategies))

	for strategyName, strategyImpl := range m.strategies {
		strategyCfg := m.strategyConfigs[strategyName]

		go func() {
			slog.Debug("[RAG Manager] Initializing strategy",
				"rag_name", m.name,
				"strategy", strategyName,
				"num_docs", len(strategyCfg.Docs),
				"chunk_size", strategyCfg.ChunkSize,
				"chunk_overlap", strategyCfg.ChunkOverlap)

			err := strategyImpl.Initialize(ctx, strategyCfg.Docs, strategyCfg.ChunkSize, strategyCfg.ChunkOverlap, strategyCfg.RespectWordBoundaries)
			if err != nil {
				slog.Error("[RAG Manager] Strategy initialization failed",
					"rag_name", m.name,
					"strategy", strategyName,
					"error", err)
			} else {
				slog.Info("[RAG Manager] Strategy initialized successfully",
					"rag_name", m.name,
					"strategy", strategyName)
			}
			resultsChan <- result{strategyName: strategyName, err: err}
		}()
	}

	// Wait for all strategies to complete
	var firstError error
	for range len(m.strategies) {
		res := <-resultsChan
		if res.err != nil && firstError == nil {
			firstError = res.err
		}
	}

	if firstError != nil {
		return fmt.Errorf("one or more strategies failed to initialize: %w", firstError)
	}

	slog.Info("[RAG Manager] Initialization complete",
		"rag_name", m.name)

	return nil
}

// Query searches for relevant documents using all configured strategies
// If multiple strategies are configured, results are combined using the fusion strategy
func (m *Manager) Query(ctx context.Context, query string) ([]database.SearchResult, error) {
	slog.Debug("[RAG Manager] Starting query",
		"rag_name", m.name,
		"num_strategies", len(m.strategies),
		"query_length", len(query))

	// Single retrieval strategy
	if len(m.strategies) == 1 {
		for strategyName, strategyImpl := range m.strategies {
			strategyCfg := m.strategyConfigs[strategyName]

			slog.Debug("[RAG Manager] Single strategy query",
				"rag_name", m.name,
				"strategy", strategyName,
				"strategy_limit", strategyCfg.Limit,
				"strategy_threshold", strategyCfg.Threshold)

			// For single strategy, use global result limit if defined
			limit := m.config.Results.Limit
			if limit == 0 {
				limit = strategyCfg.Limit
			}

			results, err := strategyImpl.Query(ctx, query, limit, strategyCfg.Threshold)
			if err != nil {
				slog.Error("[RAG Manager] Strategy query failed",
					"rag_name", m.name,
					"strategy", strategyName,
					"error", err)
				return nil, err
			}

			slog.Debug("[RAG Manager] Single strategy results",
				"rag_name", m.name,
				"strategy", strategyName,
				"num_results", len(results))

			// Reconstruct full documents if configured
			if m.config.Results.ReturnFullContent {
				results = m.reconstructFullDocuments(ctx, results)
			}

			if m.config.Results.Deduplicate {
				results = m.deduplicateResults(results)
				slog.Debug("[RAG Manager] Deduplicated single-strategy results",
					"rag_name", m.name,
					"strategy", strategyName,
					"num_results", len(results))
			}

			return results, nil
		}
	}

	// Multi-strategy - query all in parallel with per-strategy limits, then fuse results
	slog.Debug("[RAG Manager] Multi-strategy query (hybrid)",
		"rag_name", m.name,
		"strategies", getStrategyNames(m.strategies))

	type strategyResult struct {
		name    string
		results []database.SearchResult
		err     error
	}

	resultsChan := make(chan strategyResult, len(m.strategies))

	// Launch parallel queries based on the available retrieval strategies
	for strategyName, strategyImpl := range m.strategies {
		strategyCfg := m.strategyConfigs[strategyName]

		slog.Debug("[RAG Manager] Launching parallel query for strategy",
			"rag_name", m.name,
			"strategy", strategyName,
			"strategy_limit", strategyCfg.Limit,
			"strategy_threshold", strategyCfg.Threshold)

		go func(name string, strategyImpl strategy.Strategy, cfg strategy.Config) {
			results, err := strategyImpl.Query(ctx, query, cfg.Limit, cfg.Threshold)
			resultsChan <- strategyResult{
				name:    name,
				results: results,
				err:     err,
			}
		}(strategyName, strategyImpl, strategyCfg)
	}

	// Collect results from all strategies
	strategyResults := make(map[string][]database.SearchResult)
	for range len(m.strategies) {
		result := <-resultsChan

		if result.err != nil {
			slog.Error("[RAG Manager] Strategy query failed",
				"rag_name", m.name,
				"strategy", result.name,
				"error", result.err)
			return nil, fmt.Errorf("strategy %s failed: %w", result.name, result.err)
		}

		slog.Debug("[RAG Manager] Strategy returned results",
			"rag_name", m.name,
			"strategy", result.name,
			"num_results", len(result.results),
			"limit_was", m.strategyConfigs[result.name].Limit)

		strategyResults[result.name] = result.results
	}

	// Fuse results from all strategies
	slog.Debug("[RAG Manager] Starting fusion",
		"rag_name", m.name,
		"num_strategies", len(strategyResults))

	// Safety check: fusion should never be nil with multiple strategies
	if m.fusion == nil {
		return nil, fmt.Errorf("fusion strategy is nil but multiple strategies are configured (this is a bug)")
	}

	fusedResults, err := m.fusion.Fuse(strategyResults)
	if err != nil {
		slog.Error("[RAG Manager] Fusion failed",
			"rag_name", m.name,
			"error", err)
		return nil, fmt.Errorf("failed to fuse results: %w", err)
	}

	slog.Debug("[RAG Manager] Fusion complete",
		"rag_name", m.name,
		"fused_results", len(fusedResults),
		"result_limit", m.config.Results.Limit)

	// Apply result limit if configured
	if limit := m.config.Results.Limit; limit > 0 && len(fusedResults) > limit {
		slog.Debug("[RAG Manager] Truncating to result limit",
			"rag_name", m.name,
			"before", len(fusedResults),
			"after", limit)
		fusedResults = fusedResults[:limit]
	}

	// Reconstruct full documents if configured
	if m.config.Results.ReturnFullContent {
		fusedResults = m.reconstructFullDocuments(ctx, fusedResults)
	}

	// Optionally deduplicate based on the final content that will be returned
	// (full documents or chunks).
	if m.config.Results.Deduplicate {
		fusedResults = m.deduplicateResults(fusedResults)
		slog.Debug("[RAG Manager] Deduplicated fused results",
			"rag_name", m.name,
			"num_results", len(fusedResults))
	}

	// TODO: Track and emit query embedding usage
	// For queries during agent execution, usage should be added to agent's session
	// This requires passing session context through the RAG tool

	return fusedResults, nil
}

// Helper to get strategy names for logging
func getStrategyNames(stratMap map[string]strategy.Strategy) []string {
	names := make([]string, 0, len(stratMap))
	for name := range stratMap {
		names = append(names, name)
	}
	return names
}

// CheckAndReindexChangedFiles checks for file changes and re-indexes if needed
func (m *Manager) CheckAndReindexChangedFiles(ctx context.Context) error {
	for strategyName, strategyImpl := range m.strategies {
		strategyCfg := m.strategyConfigs[strategyName]
		if err := strategyImpl.CheckAndReindexChangedFiles(ctx, strategyCfg.Docs, strategyCfg.ChunkSize, strategyCfg.ChunkOverlap, strategyCfg.RespectWordBoundaries); err != nil {
			return fmt.Errorf("strategy %s failed: %w", strategyName, err)
		}
	}
	return nil
}

// StartFileWatcher starts monitoring files and directories for changes
func (m *Manager) StartFileWatcher(ctx context.Context) error {
	for strategyName, strategyImpl := range m.strategies {
		strategyCfg := m.strategyConfigs[strategyName]
		if err := strategyImpl.StartFileWatcher(ctx, strategyCfg.Docs, strategyCfg.ChunkSize, strategyCfg.ChunkOverlap, strategyCfg.RespectWordBoundaries); err != nil {
			return fmt.Errorf("strategy %s failed: %w", strategyName, err)
		}
	}
	return nil
}

// Events returns the event channel shared by all strategies for this manager.
func (m *Manager) Events() <-chan strategy.Event {
	return m.events
}

// Close closes the manager and releases resources
func (m *Manager) Close() error {
	slog.Debug("[RAG Manager] Closing manager", "rag_name", m.name)

	var firstErr error

	// Close all strategies (which closes their databases and file watchers)
	for strategyName, strategyImpl := range m.strategies {
		if err := strategyImpl.Close(); err != nil {
			slog.Error("[RAG Manager] Failed to close strategy",
				"rag_name", m.name,
				"strategy", strategyName,
				"error", err)
			if firstErr == nil {
				firstErr = fmt.Errorf("failed to close strategy %s: %w", strategyName, err)
			}
		}
	}

	slog.Debug("[RAG Manager] Manager closed", "rag_name", m.name)
	return firstErr
}

// Name returns the RAG source name
func (m *Manager) Name() string {
	return m.name
}

// Description returns the RAG source description
func (m *Manager) Description() string {
	return m.config.Description
}

// deduplicateResults removes duplicate entries from the result set.
// Entries are considered duplicates if they have identical content in Document.Content.
// The first occurrence (highest-ranked result) is kept.
func (m *Manager) deduplicateResults(results []database.SearchResult) []database.SearchResult {
	if len(results) == 0 {
		return results
	}

	seen := make(map[string]struct{}, len(results))
	deduped := make([]database.SearchResult, 0, len(results))

	for _, r := range results {
		key := r.Document.Content
		if _, ok := seen[key]; ok {
			continue
		}
		seen[key] = struct{}{}
		deduped = append(deduped, r)
	}

	return deduped
}

// reconstructFullDocuments replaces chunk content with full document content by reading files directly
func (m *Manager) reconstructFullDocuments(_ context.Context, results []database.SearchResult) []database.SearchResult {
	if len(results) == 0 {
		return results
	}

	slog.Debug("[RAG Manager] Reading full documents from disk",
		"rag_name", m.name,
		"num_results", len(results))

	fullContentCache := make(map[string]string)

	for i := range results {
		sourcePath := results[i].Document.SourcePath

		fullContent, ok := fullContentCache[sourcePath]
		if !ok {
			content, err := m.readFile(sourcePath)
			if err != nil {
				slog.Warn("[RAG Manager] Failed to read full document, keeping chunk",
					"rag_name", m.name,
					"source_path", sourcePath,
					"error", err)
				continue
			}

			fullContent = content
			fullContentCache[sourcePath] = fullContent

			slog.Debug("[RAG Manager] Read full document",
				"rag_name", m.name,
				"source_path", sourcePath,
				"length", len(fullContent))
		}

		results[i].Document.Content = fullContent
		results[i].Document.ChunkIndex = 0 // Reset to 0 since it's now the full document
	}

	return results
}

// readFile reads the content of a file
func (m *Manager) readFile(path string) (string, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		return "", fmt.Errorf("failed to read file %s: %w", path, err)
	}
	return string(data), nil
}

// GetAbsolutePaths converts doc paths to absolute paths
func GetAbsolutePaths(basePath string, docPaths []string) []string {
	var absPaths []string
	for _, p := range docPaths {
		if filepath.IsAbs(p) {
			absPaths = append(absPaths, p)
		} else {
			absPaths = append(absPaths, filepath.Join(basePath, p))
		}
	}
	return absPaths
}
